"""
Comprehensive tests for models/logging.py

Targets:
- StreamToLogger: write(), flush(), isatty()
- WebSocketConnectionManager: connect(), disconnect(), broadcast()
- WebSocketLogHandler: emit()
- Edge cases: exception handling, asyncio loop detection

Coverage target: 70-80% (40-50 statements out of 57 missing)
"""

import json
import logging
from unittest.mock import AsyncMock, MagicMock, call, patch

import pytest
from fastapi import WebSocket, WebSocketDisconnect

from models.logging import (
    StreamToLogger,
    WebSocketConnectionManager,
    WebSocketLogHandler,
)

# ==================== StreamToLogger TESTS ====================


def test_streamtologger_init():
    """Test StreamToLogger initialization."""
    logger = logging.getLogger("test")
    stream = StreamToLogger(logger, log_level=logging.DEBUG)

    assert stream.logger == logger
    assert stream.log_level == logging.DEBUG
    assert stream.linebuf == ""


def test_streamtologger_write_single_line():
    """Test writing a single complete line."""
    logger = MagicMock()
    stream = StreamToLogger(logger, log_level=logging.INFO)

    stream.write("Hello world\n")

    logger.log.assert_called_once_with(logging.INFO, "Hello world")
    assert stream.linebuf == ""


def test_streamtologger_write_multiple_lines():
    """Test writing multiple lines at once."""
    logger = MagicMock()
    stream = StreamToLogger(logger, log_level=logging.INFO)

    stream.write("Line 1\nLine 2\nLine 3\n")

    assert logger.log.call_count == 3
    calls = logger.log.call_args_list
    assert calls[0] == call(logging.INFO, "Line 1")
    assert calls[1] == call(logging.INFO, "Line 2")
    assert calls[2] == call(logging.INFO, "Line 3")
    assert stream.linebuf == ""


def test_streamtologger_write_incomplete_line():
    """Test writing incomplete line (no newline) - should buffer."""
    logger = MagicMock()
    stream = StreamToLogger(logger, log_level=logging.INFO)

    stream.write("Incomplete")

    logger.log.assert_not_called()
    assert stream.linebuf == "Incomplete"


def test_streamtologger_write_buffered_continuation():
    """Test continuing a buffered line."""
    logger = MagicMock()
    stream = StreamToLogger(logger, log_level=logging.INFO)

    stream.write("Hello ")
    stream.write("world\n")

    logger.log.assert_called_once_with(logging.INFO, "Hello world")
    assert stream.linebuf == ""


def test_streamtologger_write_carriage_return():
    """Test writing line with carriage return."""
    logger = MagicMock()
    stream = StreamToLogger(logger, log_level=logging.INFO)

    stream.write("Line with CR\r")

    logger.log.assert_called_once_with(logging.INFO, "Line with CR")
    assert stream.linebuf == ""


def test_streamtologger_write_exception():
    """Test exception handling during write."""
    logger = MagicMock()
    logger.log.side_effect = Exception("Log error")
    stream = StreamToLogger(logger, log_level=logging.INFO)

    with patch("sys.__stderr__"):
        stream.write("Test line\n")

        # Should not raise, just print to stderr
        # Note: We can't easily verify the print call, but no exception should propagate


def test_streamtologger_flush_empty_buffer():
    """Test flushing with empty buffer."""
    logger = MagicMock()
    stream = StreamToLogger(logger, log_level=logging.INFO)

    stream.flush()

    logger.log.assert_not_called()
    assert stream.linebuf == ""


def test_streamtologger_flush_with_content():
    """Test flushing buffer with content."""
    logger = MagicMock()
    stream = StreamToLogger(logger, log_level=logging.INFO)

    stream.linebuf = "Buffered content"
    stream.flush()

    logger.log.assert_called_once_with(logging.INFO, "Buffered content")
    assert stream.linebuf == ""


def test_streamtologger_flush_exception():
    """Test exception handling during flush."""
    logger = MagicMock()
    logger.log.side_effect = Exception("Flush error")
    stream = StreamToLogger(logger, log_level=logging.INFO)
    stream.linebuf = "Content"

    with patch("sys.__stderr__"):
        stream.flush()

        # Should not raise, but buffer is NOT cleared on exception
        # (buffer clearing happens after logger.log, which raised)
        assert stream.linebuf == "Content"


def test_streamtologger_isatty():
    """Test isatty returns False."""
    logger = MagicMock()
    stream = StreamToLogger(logger)

    assert stream.isatty() is False


# ==================== WebSocketConnectionManager TESTS ====================


@pytest.mark.asyncio
async def test_websocketmanager_init():
    """Test WebSocketConnectionManager initialization."""
    manager = WebSocketConnectionManager()

    assert manager.active_connections == {}


@pytest.mark.asyncio
async def test_websocketmanager_connect_success():
    """Test successful WebSocket connection."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.connect("client1", ws)

    ws.accept.assert_called_once()
    assert "client1" in manager.active_connections
    assert manager.active_connections["client1"] == ws

    # Should send welcome message
    ws.send_text.assert_called_once()
    sent_data = json.loads(ws.send_text.call_args[0][0])
    assert sent_data["type"] == "connection_status"
    assert sent_data["status"] == "connected"

    # Should log connection
    mock_logger.info.assert_called_once()


@pytest.mark.asyncio
async def test_websocketmanager_connect_send_exception():
    """Test WebSocket connection when send_text fails."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)
    ws.send_text.side_effect = Exception("Send failed")

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.connect("client2", ws)

    # Connection should still be stored
    assert "client2" in manager.active_connections

    # Should log warning about failed send
    mock_logger.warning.assert_called_once()


def test_websocketmanager_disconnect_existing():
    """Test disconnecting an existing client."""
    manager = WebSocketConnectionManager()
    ws = MagicMock()
    manager.active_connections["client1"] = ws

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        manager.disconnect("client1")

    assert "client1" not in manager.active_connections
    mock_logger.info.assert_called_once()


def test_websocketmanager_disconnect_nonexistent():
    """Test disconnecting a non-existent client (should do nothing)."""
    manager = WebSocketConnectionManager()

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        manager.disconnect("nonexistent")

    # Should not log (client not found)
    mock_logger.info.assert_not_called()


@pytest.mark.asyncio
async def test_websocketmanager_broadcast_no_connections():
    """Test broadcasting with no active connections (early return)."""
    manager = WebSocketConnectionManager()

    # Should not raise
    await manager.broadcast("Test message")


@pytest.mark.asyncio
async def test_websocketmanager_broadcast_single_client():
    """Test broadcasting to a single client."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)
    manager.active_connections["client1"] = ws

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.broadcast("Test message")

    ws.send_text.assert_called_once_with("Test message")
    assert "client1" in manager.active_connections  # Not disconnected


@pytest.mark.asyncio
async def test_websocketmanager_broadcast_multiple_clients():
    """Test broadcasting to multiple clients."""
    manager = WebSocketConnectionManager()
    ws1 = AsyncMock(spec=WebSocket)
    ws2 = AsyncMock(spec=WebSocket)
    manager.active_connections["client1"] = ws1
    manager.active_connections["client2"] = ws2

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.broadcast("Test message")

    ws1.send_text.assert_called_once_with("Test message")
    ws2.send_text.assert_called_once_with("Test message")


@pytest.mark.asyncio
async def test_websocketmanager_broadcast_websocketdisconnect():
    """Test broadcasting when client disconnects during send."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)
    ws.send_text.side_effect = WebSocketDisconnect()
    manager.active_connections["client1"] = ws

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.broadcast("Test message")

    # Client should be disconnected
    assert "client1" not in manager.active_connections
    mock_logger.info.assert_called()


@pytest.mark.asyncio
async def test_websocketmanager_broadcast_runtimeerror_closed():
    """Test broadcasting with RuntimeError 'Connection is closed'."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)
    ws.send_text.side_effect = RuntimeError("Connection is closed")
    manager.active_connections["client1"] = ws

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.broadcast("Test message")

    # Client should be disconnected
    assert "client1" not in manager.active_connections
    # Should log the closed connection
    assert any(
        "已关闭" in str(call) or "client1" in str(call)
        for call in mock_logger.info.call_args_list
    )


@pytest.mark.asyncio
async def test_websocketmanager_broadcast_runtimeerror_other():
    """Test broadcasting with RuntimeError (not connection closed)."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)
    ws.send_text.side_effect = RuntimeError("Some other error")
    manager.active_connections["client1"] = ws

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.broadcast("Test message")

    # Client should be disconnected
    assert "client1" not in manager.active_connections
    # Should log error
    mock_logger.error.assert_called()


@pytest.mark.asyncio
async def test_websocketmanager_broadcast_generic_exception():
    """Test broadcasting with generic exception."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)
    ws.send_text.side_effect = ValueError("Unexpected error")
    manager.active_connections["client1"] = ws

    with patch("logging.getLogger") as mock_get_logger:
        mock_logger = MagicMock()
        mock_get_logger.return_value = mock_logger

        await manager.broadcast("Test message")

    # Client should be disconnected
    assert "client1" not in manager.active_connections
    # Should log error
    mock_logger.error.assert_called()


# ==================== WebSocketLogHandler TESTS ====================


def test_websocketloghandler_init():
    """Test WebSocketLogHandler initialization."""
    manager = MagicMock()
    handler = WebSocketLogHandler(manager)

    assert handler.manager == manager
    assert isinstance(handler.formatter, logging.Formatter)


@pytest.mark.asyncio
async def test_websocketloghandler_emit_with_active_connections():
    """Test emitting log record with active connections."""
    manager = MagicMock()
    manager.active_connections = {"client1": MagicMock()}
    handler = WebSocketLogHandler(manager)

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )

    # Mock asyncio.get_running_loop to simulate running in event loop
    mock_loop = MagicMock()
    mock_loop.create_task = MagicMock()

    with patch("asyncio.get_running_loop", return_value=mock_loop):
        handler.emit(record)

    # Should create task for broadcast
    mock_loop.create_task.assert_called_once()


def test_websocketloghandler_emit_no_connections():
    """Test emitting log record with no active connections."""
    manager = MagicMock()
    manager.active_connections = {}
    handler = WebSocketLogHandler(manager)

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )

    # Should not raise
    handler.emit(record)


def test_websocketloghandler_emit_no_manager():
    """Test emitting log record with no manager."""
    handler = WebSocketLogHandler(None)  # type: ignore[arg-type]

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )

    # Should not raise
    handler.emit(record)


def test_websocketloghandler_emit_no_running_loop():
    """Test emitting log record when no event loop is running."""
    manager = MagicMock()
    manager.active_connections = {"client1": MagicMock()}
    handler = WebSocketLogHandler(manager)

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )

    # Mock asyncio.get_running_loop to raise RuntimeError (no loop)
    with patch("asyncio.get_running_loop", side_effect=RuntimeError("No running loop")):
        # Should not raise - catches RuntimeError
        handler.emit(record)


def test_websocketloghandler_emit_format_exception():
    """Test emitting log record when formatting fails."""
    manager = MagicMock()
    manager.active_connections = {"client1": MagicMock()}
    handler = WebSocketLogHandler(manager)

    # Make format() raise exception
    handler.format = MagicMock(side_effect=Exception("Format error"))

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Test message",
        args=(),
        exc_info=None,
    )

    with patch("sys.__stderr__"):
        # Should not raise, just print to stderr
        handler.emit(record)


# ==================== INTEGRATION TESTS ====================


@pytest.mark.asyncio
async def test_websocket_logging_integration():
    """Test full integration: handler -> manager -> websocket."""
    manager = WebSocketConnectionManager()
    ws = AsyncMock(spec=WebSocket)

    # Connect a client
    with patch("logging.getLogger"):
        await manager.connect("client1", ws)

    # Create handler and logger
    handler = WebSocketLogHandler(manager)
    logger = logging.getLogger("integration_test")
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)

    # Create log record
    record = logging.LogRecord(
        name="integration_test",
        level=logging.INFO,
        pathname="test.py",
        lineno=1,
        msg="Integration test message",
        args=(),
        exc_info=None,
    )

    # Mock event loop
    mock_loop = AsyncMock()

    with patch("asyncio.get_running_loop", return_value=mock_loop):
        handler.emit(record)

    # Should create task
    mock_loop.create_task.assert_called_once()

    logger.removeHandler(handler)


def test_streamtologger_integration_with_real_logger():
    """Test StreamToLogger with a real logger instance."""
    logger = logging.getLogger("stream_integration")
    logger.handlers.clear()
    handler = logging.StreamHandler()
    logger.addHandler(handler)
    logger.setLevel(logging.INFO)

    stream = StreamToLogger(logger, log_level=logging.INFO)

    # Write some content
    stream.write("Line 1\n")
    stream.write("Partial ")
    stream.write("line\n")
    stream.flush()

    # Should not raise
    logger.handlers.clear()
